作者:清响11 | 来源:互联网 | 2023-09-16 13:50
篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何合理的使用Redis内存相关的知识,希望对你有一定的参考价值。
简述
众所周知,Redis 的性能之所以高,原因之一就是它的数据都存储在内存中,硬件服务器的内存资源相比于磁盘来说,还是比较昂贵,在成本与性能中我们该如何去权衡,那在使用 Redis 时,如何做到保证良好的性能并更节省内存呢?
内存模型
模型简介
Redis的数据存储,主要涉及到内存分配器(默认jemalloc),简单动态字符串(SDS),5种数据对象类型(字符串、哈希,列表,集合,有序集合),内部编码,redisobject,下面以set hello word操作,写入一条数据为例,做个简单的分析:
- dictEntry:每个键值对都会有一个dictEntry结构体,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关。
- Key:Key ”hello” 并不是直接以字符串存储,而是存储在SDS结构中。
- redisObject:Value 值“world”不是直接以字符串存储,是存储在redisObject中。我们常说的物种数据类型也都是都是通过redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。不过可以看出,字符串对象虽然经过了redisObject的包装,但仍然需要通过SDS存储。
- SDS:Redis没有直接使用C字符串(即以空字符’\\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。
- Jemalloc:无论是DictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。以DictEntry对象为例,有3个指针组成,在64位机器下占24个字节,jemalloc会为它分配32字节大小的内存单元。
数据占用
jemalloc内存分配情况如下,在分配内存块时会分配大于实际值的2次n的值
数据类型 | 占用量 |
dictEntry | 24字节,jemalloc会分配32字节的内存块 |
redisObject | 16字节 |
key | key的长度+9,jemalloc分配>=该值的2n的值 |
value | value的长度+9,jemalloc分配>=该值的2n的值 |
key的个数 | 所有key的个数 |
bucket个数 | 大于key的个数的2n次方,例如key个数是2000,那么bucket=2048 |
指针大小 | 8byte |
String
一个set命令会产生一下几个内存消耗的结构:
- 1个dictEntry结构,24字节,负责保存当前的哈希对象;
- 1个SDS结构,(key长度 + 9)字节,用作key字符串;
- 1个SDS结构,(val长度 + 9)字节,用作key的values;
- 1个redisObject结构,16字节,指向当前key下属的dict结构;
Hash
Hash结构在使用HTable数据类型时,value并不是指向一个SDS,是有一个Dict结构。Dict结构保存了Hash对象的键值对。一个hmset命令最终会产生以下几个消耗内存的结构:
- 1个dictEntry结构,24字节,负责保存当前的哈希对象;
- 1个SDS结构,(key长度 + 9)字节,用作key字符串;
- 1个redisObject结构,16字节,指向当前key下属的dict结构;
- 1个dict结构,88字节,负责保存哈希对象的键值对;
- n个dictEntry结构,24×n字节,负责保存具体的field和value,n等于field个数;
- n个redisObject结构,16×n字节,用作field对象;
- n个redisObject结构,16×n字节,用作value对象;
- n个SDS结构,(field长度 + 9)× n字节,用作field字符串;
- n个SDS结构,(value长度 + 9)× n字节,用作value字符串;
SortedSet
一个zadd命令最终会产生以下几个消耗内存的结构:
- 1个dictEntry结构,24字节,负责保存当前的有序集合对象;
- 1个SDS结构,(key长度 + 9)字节,用作key字符串;
- 1个redisObject结构,16字节,指向当前key下属的zset结构;
- 1个zset结构,16字节,负责保存下属的dict和zskiplist结构;
- 1个dict结构,88字节,负责保存集合元素中成员到分值的映射;
- n个dictEntry结构,24×n字节,负责保存具体的成员和分值,n等于集合成员个数;
- 1个zskiplist结构,32字节,负责保存跳跃表的相关信息;
- 1个32层的zskiplistNode结构,24+16×32=536字节,用作跳跃表头结点;
- n个zskiplistNode结构,(24+16×m)×n字节,用作跳跃表节点,m等于节点层数;
- n个redisObject结构,16×n字节,用作集合中的成员对象;
- n个SDS结构,(value长度 + 9)×n字节,用作成员字符串;
List
一个rpush或者lpush命令最终会产生以下几个消耗内存的结构:
- 1个dictEntry结构,24字节,负责保存当前的列表对象;
- 1个SDS结构,(key长度 + 9)字节,用作key字符串;
- 1个redisObject结构,16字节,指向当前key下属的list结构;
- 1个list结构,48字节,负责管理链表节点;
- n个listNode结构,24×n字节,n等于value个数;
- n个redisObject结构,16×n字节,用作链表中的值对象;
- n个SDS结构,(value长度 + 9)×n字节,用作值对象指向的字符串;
优化建议
合理的控制 key 的长度
在研发业务时,需要保证 key 在简单、清晰的前提下,尽可能把 key 定义得简短一些。随着key的数量级越大,Redis 就可以节省的内存越多
利用jemalloc特性进行优化
jemalloc分配内存时数值是不连续的,因此key/value字符串变化一个字节,可能会引起占用内存很大的变动;在设计时可以利用这一点。如果key的长度如果是8个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为7个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一半。
选择合适的数据类型
String、Set 在存储 int 数据时,会采用整数编码存储。Hash、ZSet 在元素数量比较少时(可配置),会采用压缩列表(ziplist)存储,在存储比较多的数据时,才会转换为哈希表和跳表。可以节省更多空间。因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。
避免存储 bigkey
bigkey不仅会占用过多的内存,还会客户端在读书数据时,容易导致阻塞,造成性能瓶颈,对于bigkey,建议进行拆分,建议如下:
- String:大小控制在 10KB 以下
- List/Hash/Set/ZSet:元素数量控制在 1 万以下
把 Redis 当作缓存使用
你在使用 Redis 时,
- 尽可能要把它当做缓存来使用,而不是持久化的数据,不然也会存在数据风险,在写入数据时,尽可能的设置过期时间,业务应用在redis查不多数据时,应从后端数据库查询并加载到redis中。
- 尽可能的存放一些热点数据,对于不常用的数据,可以查询其他路径获取,如从对象型数据库中获取。
设置最大内存和合理的淘汰策略
Redis key 设置合理的过期时间,如果你的业务应用写入量很大,有了合理的过期时间,会将对应的数据过期掉,也可以抑制内存的快读增长
数据压缩后写入 Redis
开发在研发应用时,可以先将数据进行压缩,压缩后再写入redis,当然这会增加应用开发的复杂度和消耗过多的客户端cpu资源,需要结合业务特性进行权限 务应用中先将数据压缩,再写入到 Redis 中(例如采用 snappy、gzip 等压缩算法)。
监控内存碎片率
内存碎片率是一个重要的参数mem_fragmentation_ratio=used_memory_rss/used_memory
- 如果内存碎片率过高(jemalloc 在 1.03 左右比较正常),说明内存碎片多,内存浪费严重。并可考虑堆Redis数据库进行切换,重启,在内存中对数据进行重排,减少内存碎片。
- 如果内存碎片率小于 1,说明 Redis 内存不足,部分数据使用了虚拟内存(即 swap)。由于虚拟内存的存取速度比物理内存差很多(2-3 个数量级),此时 Redis 的访问速度可能会变得很慢。